package org.budo.graph.view.util;

import java.security.MessageDigest;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.budo.graph.service.BudoGraphService;
import org.budo.support.java.security.util.DigestUtil;
import org.budo.support.spring.util.SpringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;

import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.common.utils.NetUtils;
import com.alibaba.dubbo.common.utils.StringUtils;
import com.alibaba.dubbo.registry.NotifyListener;
import com.alibaba.dubbo.registry.RegistryService;

/**
 * @author limw
 * 
 * @see org.apache.dubbo.admin.governance.sync.RegistryServerSync
 */
public class BudoRegistryServerSync implements NotifyListener {
    private static final Logger log = LoggerFactory.getLogger(BudoRegistryServerSync.class);

    private static MessageDigest _md5MessageDigest = DigestUtil.getMessageDigestInstance("MD5");

    private static final char[] _hexCode = "0123456789ABCDEF".toCharArray();

    private static final String CATEGORY_VALUE = Constants.PROVIDERS_CATEGORY + "," //
            + Constants.CONSUMERS_CATEGORY + ","//
            + Constants.ROUTERS_CATEGORY + "," //
            + Constants.CONFIGURATORS_CATEGORY;

    private static final URL SUBSCRIBE_URL = new URL(Constants.ADMIN_PROTOCOL, NetUtils.getLocalHost(), 0, "", //
            Constants.INTERFACE_KEY, BudoGraphService.class.getName(), //
            Constants.GROUP_KEY, Constants.ANY_VALUE, //
            Constants.VERSION_KEY, Constants.ANY_VALUE, //
            Constants.CLASSIFIER_KEY, Constants.ANY_VALUE, //
            Constants.CATEGORY_KEY, CATEGORY_VALUE, //
            Constants.ENABLED_KEY, Constants.ANY_VALUE, //
            Constants.CHECK_KEY, String.valueOf(false));

    private static final Map<RegistryService, BudoRegistryServerSync> _registrySyncMap = new ConcurrentHashMap<RegistryService, BudoRegistryServerSync>();

    /**
     * Make sure ID never changed when the same url notified many times
     */
    private final ConcurrentHashMap<String, String> _urlIdsMapper = new ConcurrentHashMap<>();

    // ConcurrentMap<category, ConcurrentMap<servicename, Map<MD5, URL>>>
    private final ConcurrentMap<String, ConcurrentMap<String, Map<String, URL>>> _registryCache = new ConcurrentHashMap<>();

    public BudoRegistryServerSync(RegistryService registryService) {
        log.info("#53 subscribe, registryService=" + registryService + ", this=" + this);

        registryService.subscribe(SUBSCRIBE_URL, this);
    }

    public ConcurrentMap<String, ConcurrentMap<String, Map<String, URL>>> getRegistryCache() {
        return this._registryCache;
    }

    // Notification of of any service with any type (override、subcribe、route、provider) is full.
    public void notify(List<URL> urls) {
        if (urls == null || urls.isEmpty()) {
            return;
        }

        // Map<category, Map<servicename, Map<Long, URL>>>
        final Map<String, Map<String, Map<String, URL>>> categories = new HashMap<>();
        String interfaceName = null;
        for (URL url : urls) {
            String category = url.getParameter(Constants.CATEGORY_KEY, Constants.PROVIDERS_CATEGORY);
            if (Constants.EMPTY_PROTOCOL.equalsIgnoreCase(url.getProtocol())) { // NOTE: group and version in empty protocol is *
                ConcurrentMap<String, Map<String, URL>> services = _registryCache.get(category);
                if (services != null) {
                    String group = url.getParameter(Constants.GROUP_KEY);
                    String version = url.getParameter(Constants.VERSION_KEY);
                    // NOTE: group and version in empty protocol is *
                    if (!Constants.ANY_VALUE.equals(group) && !Constants.ANY_VALUE.equals(version)) {
                        services.remove(url.getServiceKey());
                    } else {
                        for (Map.Entry<String, Map<String, URL>> serviceEntry : services.entrySet()) {
                            String service = serviceEntry.getKey();
                            if (_getInterface(service).equals(url.getServiceInterface()) //
                                    && (Constants.ANY_VALUE.equals(group) || StringUtils.isEquals(group, _getGroup(service))) //
                                    && (Constants.ANY_VALUE.equals(version) || StringUtils.isEquals(version, _getVersion(service)))) {
                                services.remove(service);
                            }
                        }
                    }
                }
            } else {
                if (null == interfaceName || interfaceName.isEmpty()) {
                    interfaceName = url.getServiceInterface();
                }

                Map<String, Map<String, URL>> services = categories.get(category);
                if (services == null) {
                    services = new HashMap<>();
                    categories.put(category, services);
                }

                String service = url.getServiceKey();
                Map<String, URL> ids = services.get(service);
                if (ids == null) {
                    ids = new HashMap<>();
                    services.put(service, ids);
                }

                // Make sure we use the same ID for the same URL
                if (_urlIdsMapper.containsKey(url.toFullString())) {
                    ids.put(_urlIdsMapper.get(url.toFullString()), url);
                } else {
                    String md5 = _md5_16bit(url.toFullString());
                    ids.put(md5, url);
                    _urlIdsMapper.putIfAbsent(url.toFullString(), md5);
                }
            }
        }

        if (categories.size() == 0) {
            return;
        }

        for (Map.Entry<String, Map<String, Map<String, URL>>> categoryEntry : categories.entrySet()) {
            String category = categoryEntry.getKey();
            ConcurrentMap<String, Map<String, URL>> services = _registryCache.get(category);
            if (services == null) {
                services = new ConcurrentHashMap<String, Map<String, URL>>();
                _registryCache.put(category, services);
            } else {
                // Fix map can not be cleared when service is unregistered: when a unique “group/service:version”
                // service is unregistered, but we still have the same services with different version or group, so empty protocols can not be invoked.
                Set<String> keys = new HashSet<String>(services.keySet());
                for (String key : keys) {
                    if (_getInterface(key).equals(interfaceName) //
                            && !categoryEntry.getValue().entrySet().contains((Object) key)) {
                        services.remove(key);
                    }
                }
            }
            services.putAll(categoryEntry.getValue());
        }
    }

    private String _md5_16bit(String input) {
        String hash = _md5_32bit(input);
        if (hash == null) {
            return null;
        }
        return hash.substring(8, 24);
    }

    public static String _md5_32bit(String input) {
        if (input == null || input.length() == 0) {
            return null;
        }

        _md5MessageDigest.update(input.getBytes());
        byte[] digest = _md5MessageDigest.digest();
        String hash = _bytesToHexString(digest);
        return hash;
    }

    private static String _bytesToHexString(byte[] data) {
        StringBuilder stringBuilder = new StringBuilder(data.length * 2);
        for (byte b : data) {
            stringBuilder.append(_hexCode[(b >> 4) & 0xF]);
            stringBuilder.append(_hexCode[(b & 0xF)]);
        }
        return stringBuilder.toString();
    }

    private String _getVersion(String service) {
        if (service != null && service.length() > 0) {
            int i = service.lastIndexOf(':');
            if (i >= 0) {
                return service.substring(i + 1);
            }
        }
        return null;
    }

    private String _getGroup(String service) {
        if (service != null && service.length() > 0) {
            int i = service.indexOf('/');
            if (i >= 0) {
                return service.substring(0, i);
            }
        }
        return null;
    }

    private String _getInterface(String service) {
        if (service != null && service.length() > 0) {
            int i = service.indexOf('/');
            if (i >= 0) {
                service = service.substring(i + 1);
            }
            i = service.lastIndexOf(':');
            if (i >= 0) {
                service = service.substring(0, i);
            }
        }
        return service;
    }

    public static BudoRegistryServerSync getBudoRegistryServerSync(ApplicationContext applicationContext) {
        RegistryService registryService = SpringUtil.getBean(applicationContext, RegistryService.class);
        if (null == registryService) {
            log.warn("#220 RegistryService in applicationContext not found, applicationContext=" + applicationContext);
            return null;
        }

        BudoRegistryServerSync _registrySync = _registrySyncMap.get(registryService);
        if (null != _registrySync) {
            return _registrySync;
        }

        _registrySync = new BudoRegistryServerSync(registryService);
        _registrySyncMap.put(registryService, _registrySync);

        return _registrySync;
    }
}